package com.ar4j.spring;

import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.AutowireCandidateResolver;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.PriorityOrdered;
import org.springframework.util.PropertyPlaceholderHelper;

import com.ar4j.ActiveRecord;
import com.ar4j.type.ITypeConverter;
import com.ar4j.type.TypeConverterRegistry;

/**
 * A Spring factory post processor that injects itself as a delegate for resolving autowiring
 * candidates. Effectively allows Ar4J to create new read only record instances whenever
 * one needs to be autowired
 */
public class AutowiringBeanFactoryPostProcessor implements AutowireCandidateResolver, BeanFactoryPostProcessor, PriorityOrdered, InitializingBean  {
  
  /**
   * Default placeholder prefix: "${" 
   */
  public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
  
  /** 
   * Default placeholder suffix: "}" 
   */
  public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
  
  private static final String DEFAULT_VALUE_SEPARATOR = ":";
  
  private AutowireCandidateResolver delegate;
  private DefaultListableBeanFactory factory;
  private String prefix = DEFAULT_PLACEHOLDER_PREFIX;
  private String suffix = DEFAULT_PLACEHOLDER_SUFFIX;
  private String placeholderValuesBeanName;
  private Map<Class<?>, Class<?>> typeConverterMap;
  
  private PropertyPlaceholderHelper helper;
  private Properties placeholderValues;
  
  @Override
  public int getOrder() {
    return PriorityOrdered.HIGHEST_PRECEDENCE;
  }
  
  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory factoryCandidate) throws BeansException {
    if(!(factoryCandidate instanceof DefaultListableBeanFactory)) {
      throw new IllegalArgumentException("AutowiringBeanFactoryPostProcessor requires a DefaultListableBeanFactory or its subclass to operate, got: " + factoryCandidate.getClass().getName());
    }
    
    factory = (DefaultListableBeanFactory) factoryCandidate;
    if(factory.getAutowireCandidateResolver() != this) {
      this.delegate = factory.getAutowireCandidateResolver();
      factory.setAutowireCandidateResolver(this);
    }
    
  }
  
  @SuppressWarnings({ "unchecked", "rawtypes" })
  @Override
  public Object getSuggestedValue(DependencyDescriptor descriptor) {
    Object out = null;
    if(ActiveRecord.class.isAssignableFrom(descriptor.getDependencyType())) {
      Class<?> baseClass = descriptor.getDependencyType();
      
      // extract annotations (qualifiers/schema) from member
      ActiveRecordQualifier qualifierAnno = null;
      for(Annotation anno : descriptor.getAnnotations()) {
        if(ActiveRecordQualifier.class.isAssignableFrom(anno.getClass())) {
          qualifierAnno = (ActiveRecordQualifier) anno;
        }
      }
      
      out = getQualifiedReadOnlyInstance((Class) baseClass, qualifierAnno); 
    } else {
      out = delegate.getSuggestedValue(descriptor);
    }
    
    return out;
  }
  
  /**
   * Returns the qualified read only instance using the ReadOnlyActiveRecordFactory. The qualifier values are checked for place
   * holders first and replaced with live values as needed. This method is public to allow others to inject the post processor
   * and perform similar actions (i.e. spring test classes)
   */
  public <E extends ActiveRecord<E>> E getQualifiedReadOnlyInstance(Class<E> baseClass, ActiveRecordQualifier qualifierAnno) {
    
    // if given qualifier is null, try to find a qualifier on the base class itself
    if(qualifierAnno == null) {
      qualifierAnno = baseClass.getAnnotation(ActiveRecordQualifier.class);
    }
    
    createPlaceholderHelperIfNeeded();
    return ReadOnlyActiveRecordFactory.getReadOnlyInstance(factory, baseClass, getSpringQualifier(qualifierAnno), getSchemaName(qualifierAnno));
  }

  @Override
  public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
    return delegate.isAutowireCandidate(bdHolder, descriptor);
  }
  
  /**
   * Instantiates type converter mappings if any were provided and registers them in the TypeConverterRegistry
   */
  @Override
  public void afterPropertiesSet() throws Exception {
    if(typeConverterMap != null && !typeConverterMap.isEmpty()) {
      for(Map.Entry<Class<?>, Class<?>> entry : typeConverterMap.entrySet()) {
        Class<?> typeClass = entry.getKey();
        Class<?> converterClass = entry.getValue();
        if(!(ITypeConverter.class.isAssignableFrom(converterClass))) {
          throw new IllegalArgumentException("Invalid converter mapping, value is not an ITypeConverter: " + converterClass);
        }
        
        ITypeConverter<?, ?> converter = (ITypeConverter<?, ?>) converterClass.newInstance();
        TypeConverterRegistry.getInstance().registerDefaultTypeConverter(typeClass, converter);
      }
    }
  }
  
  /**
   * Creates a helper to replace placeholder values if one has not been created yet. This is done on demand to give
   * enough time for the value bean to be constructed by the context
   */
  private synchronized void createPlaceholderHelperIfNeeded() {
    if(helper == null) {
      this.helper = new PropertyPlaceholderHelper(prefix, suffix, DEFAULT_VALUE_SEPARATOR, false);
      this.placeholderValues = new Properties();
      if(StringUtils.isNotBlank(placeholderValuesBeanName)) {
        placeholderValues = factory.getBean(placeholderValuesBeanName, Properties.class);
      }
    }
  }
  
  /**
   * @return the springQualifier annotation value with any placeholders replaced by real values
   */
  private String getSpringQualifier(ActiveRecordQualifier qualifier) {
    String out = null;
    if(qualifier != null && StringUtils.isNotBlank(qualifier.springQualifier())) {
      out = helper.replacePlaceholders(qualifier.springQualifier(), placeholderValues);
    }
  
    return out;
  }

  /**
   * @return the schemaName annotation value with any placeholders replaced by real values
   */
  private String getSchemaName(ActiveRecordQualifier qualifier) {
    String out = null;
    if(qualifier != null && StringUtils.isNotBlank(qualifier.schemaName())) {
      out = helper.replacePlaceholders(qualifier.schemaName(), placeholderValues);
    }
  
    return out;
  }

  public String getPrefix() {
    return prefix;
  }

  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  public String getSuffix() {
    return suffix;
  }

  public void setSuffix(String suffix) {
    
    this.suffix = suffix;
  }

  public String getPlaceholderValuesBeanName() {
    return placeholderValuesBeanName;
  }

  public void setPlaceholderValuesBeanName(String placeholderValuesBeanName) {
    this.placeholderValuesBeanName = placeholderValuesBeanName;
  }

  public Map<Class<?>, Class<?>> getTypeConverterMap() {
    return typeConverterMap;
  }

  public void setTypeConverterMap(Map<Class<?>, Class<?>> typeConverterMap) {
    this.typeConverterMap = typeConverterMap;
  }
    
}
